﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Runtime.InteropServices;
using System.Diagnostics;
using System.Reflection;
using System.ComponentModel;

namespace CodePlex.KeyedList
{

	/// <summary>
	/// Provides a class for a collection whose keys are embedded in the values.
	/// </summary>
	/// <typeparam name="TKey">The type of keys in the collection.</typeparam>
	/// <typeparam name="TItem">The type of items in the collection.</typeparam>
	[Serializable, ComVisible(false), DebuggerDisplay("Count = {Count}")]
	public class KeyedList<TKey, TItem> : KeyedCollection<TKey, TItem>, IList<TItem>, ICollection<TItem>, IEnumerable<TItem>
	{

		#region Type Initializer

		/// <summary>
		/// Initializes the KeyedList for use with types TKey as key and TItem as the object
		/// </summary>
		static KeyedList()
		{
			InitializeKeyForItemDelegate();
		}

		#endregion

		#region KeyForItemDelegate

		private static Exception s_initializerKeyDelegateException;
		private static GetKeyForItemDelegate<TKey, TItem> s_getKeyForItemDelegate;

		/// <summary>
		/// If an exception has occurred during initialization of the KeyDelegate, it will be thrown
		/// </summary>
		private void CheckRepressedKeyDelegateException()
		{
			if(s_initializerKeyDelegateException != null)
				throw s_initializerKeyDelegateException;
		}

		/// <summary>
		/// Initializes the KeyedList for use with types TKey as key and TItem as the object
		/// </summary>
		private static void InitializeKeyForItemDelegate()
		{
			try
			{
				Type itemType = typeof(TItem);
				Type keyType = typeof(TKey);

				// first check if the TItem implements the IKeyedItem interface
				foreach(Type itype in itemType.GetInterfaces())
				{
					if(itype.Equals(typeof(IKeyedItem<TKey>)))
					{
						// if so the the delegate is obvious
						s_getKeyForItemDelegate = (obj => ((IKeyedItem<TKey>)obj).Key);
						return;
					}
				}

				// next we can check for a single property of the Key type, 
				// - and marked with DataObjectField attribute
				PropertyInfo[] publicInstanceProperties = itemType.GetProperties(BindingFlags.Instance | BindingFlags.Public);

				// flag to mark whether to key property is found
				bool keyPropertyfound = false;
				PropertyInfo keyProperty = null;

				foreach(PropertyInfo property in publicInstanceProperties)
				{
					#region Check for DataObjectFieldAttributes

					// we dont want properties that we cannot read value from.
					if(!property.CanRead)
					{
						continue;
					}

					// we dont want indexers
					if(property.GetIndexParameters().Length > 0)
					{
						continue;
					}

					// if the type is not right...
					if(keyType != property.PropertyType)
					{
						continue;
					}

					// let's check for DataObjectField
					foreach(DataObjectFieldAttribute attribute in property.GetCustomAttributes(
						typeof(DataObjectFieldAttribute), true))
					{
						if(attribute != null && attribute.PrimaryKey)
						{
							// check that no more than one property could be the one we're after
							if(keyPropertyfound)
							{
								string message1 = SR.Error_TooManyPrimaryKeyProperties(itemType, keyType);
								s_initializerKeyDelegateException = new ApplicationException(message1);
								return;
							}

							keyProperty = property;
							keyPropertyfound = true;
						}
					}

					#endregion
				}

				if(keyPropertyfound && keyProperty != null)
				{
					s_getKeyForItemDelegate = (obj => (TKey)keyProperty.GetValue(obj, null));
					return;
				}
				
				string message2 = SR.Error_ItemNotValidAsKeyedItemByDefault(itemType, keyType);
				s_initializerKeyDelegateException = new ApplicationException(message2);
				return;


			}
			catch(Exception exception)
			{
				s_initializerKeyDelegateException = exception;
			}
		}

		#endregion

		#region GetKeyForItem

		private GetKeyForItemDelegate<TKey, TItem> _getKeyForItemDelegate;

		/// <summary>
		/// Implementation of the KeyedCollection.GetKeyForItem()
		/// </summary>
		/// <param name="item"></param>
		/// <returns></returns>
		protected override sealed TKey GetKeyForItem(TItem item)
		{
			return _getKeyForItemDelegate(item);
		}

		#endregion

		#region Ctor

		/// <summary>
		/// Returns a new instance of the KeyedList Class, with default properties
		/// </summary>
		public KeyedList()
			: this(null, null, 0)
		{
		}


		/// <summary>
		/// Returns a new instance of the KeyedList Class, 
		/// allowing you to specify the way the TKey keys are retrived from the TItem items
		/// </summary>
		/// <param name="keyForItemDelegate">the delegate that will retrive key values</param>
		public KeyedList(GetKeyForItemDelegate<TKey, TItem> keyForItemDelegate)
			: this(null, keyForItemDelegate, 0)
		{
		}


		/// <summary>
		/// Returns a new instance of the KeyedList Class, with a custom IEqualityComparer for the keyvalues
		/// </summary>
		/// <param name="comparer">The implementation of the System.Collections.Generic.IEqualityComparer</param>
		public KeyedList(IEqualityComparer<TKey> comparer)
			: this(comparer, null, 0)
		{
		}

		/// <summary>
		/// Returns a new instance of the KeyedList Class
		/// </summary>
		/// <param name="dictionaryCreationThreshold">
		/// The number of elements the collection can hold without creating a lookup
		/// dictionary (0 creates the lookup dictionary when the first item is added),
		/// or –1 to specify that a lookup dictionary is never created.
		/// </param>
		public KeyedList(int dictionaryCreationThreshold)
			: this(null, null, dictionaryCreationThreshold)
		{
		}


		/// <summary>
		/// Returns a new instance of the KeyedList Class, 
		/// with a custom IEqualityComparer for the keyvalues
		/// and allowing you to specify the way the TKey keys are retrived from the TItem items
		/// </summary>
		/// <param name="comparer">The implementation of the System.Collections.Generic.IEqualityComparer</param>
		/// <param name="keyForItemDelegate">the delegate that will retrive key values</param>
		public KeyedList(
			IEqualityComparer<TKey> comparer,
			GetKeyForItemDelegate<TKey, TItem> keyForItemDelegate
			)
			: this(comparer, keyForItemDelegate, 0)
		{
		}



		/// <summary>
		/// Returns a new instance of the KeyedList Class, allowing you to specify all values
		/// </summary>
		/// <param name="comparer">The implementation of the System.Collections.Generic.IEqualityComparer</param>
		/// <param name="keyForItemDelegate">the delegate that will retrive key values</param>
		/// <param name="dictionaryCreationThreshold">
		/// The number of elements the collection can hold without creating a lookup
		/// dictionary (0 creates the lookup dictionary when the first item is added),
		/// or –1 to specify that a lookup dictionary is never created.
		/// </param>
		public KeyedList(
			IEqualityComparer<TKey> comparer,
			GetKeyForItemDelegate<TKey, TItem> keyForItemDelegate,
			int dictionaryCreationThreshold
			)
			: base(comparer, dictionaryCreationThreshold)
		{

			// if the keyForItemDelegate is specified, we use it
			if(keyForItemDelegate != null)
			{
				this._getKeyForItemDelegate = keyForItemDelegate;
			}
			else
			{
				// otherwise we use the one found during initialization
				this.CheckRepressedKeyDelegateException();
				this._getKeyForItemDelegate = s_getKeyForItemDelegate;
			}

		}

		#endregion

		#region ICollection<TItem> Members

		/// <summary>
		/// Removes the element with the specified key from this collection.
		/// Ensures that even the object reference is not the same, an object with the same key will also be removed
		/// </summary>
		/// <param name="item">The item to remove</param>
		/// <returns></returns>
		public new bool Remove(TItem item)
		{
			// 
			if(!object.Equals(item, default(TItem)))
			{
				return this.Remove(this.GetKeyForItem(item));
			}
			return false;
		}

		bool ICollection<TItem>.Remove(TItem item)
		{
			return this.Remove(item);
		}


		/// <summary>
		/// Determines whether the collection contains an element.
		/// the key of the element is used to determine this.
		/// </summary>
		/// <param name="item"></param>
		/// <returns></returns>
		public new bool Contains(TItem item)
		{
			
			if(!object.Equals(item, default(TItem)))
			{
				return this.Contains(this.GetKeyForItem(item));
			}
			return false;
		}

		bool ICollection<TItem>.Contains(TItem item)
		{
			return this.Contains(item);
		}

		/// <summary>
		/// Returns the index of the item with the specified Key
		/// </summary>
		/// <param name="key"></param>
		/// <returns></returns>
		public int IndexOf(TKey key)
		{
			return base.IndexOf(this[key]);
		}

		#endregion
		
		#region KeyedList Extra Functionlity

		/// <summary>
		/// Asks for and retrives a value in one operation
		/// </summary>
		/// <param name="key"></param>
		/// <param name="value"></param>
		/// <returns></returns>
		public bool TryGetValue(TKey key, out TItem value)
		{
			if(this.Contains(key))
			{
				value = this[key];
				return true;
			}
			value = default(TItem);
			return false;
		}

		/// <summary>
		/// Adds a range of TItem 
		/// </summary>
		/// <param name="collection"></param>
		public void AddRange(IEnumerable<TItem> collection)
		{
			lock(this)
			{
				foreach(TItem item in collection)
				{
					base.Add(item);
				}
			}
		}
		


		#endregion
	
		#region Generic List Extras


		/// <summary>
		/// Determines whether the List contains elements that match the 
		/// conditions defined by the specified predicate.
		/// </summary>
		/// <param name="match"></param>
		/// <returns></returns>
		public bool Exists(Predicate<TItem> match)
		{
			return ((List<TItem>)this.Items).Exists(match);
		}

		/// <summary>
		/// Searches for an element that matches the conditions defined by the specified predicate,
		/// and returns the first occurrence within the entire List
		/// </summary>
		/// <param name="match"></param>
		/// <returns></returns>
		public TItem Find(Predicate<TItem> match)
		{
			return ((List<TItem>)this.Items).Find(match);
		}

		/// <summary>
		/// Retrieves all the elements that match the conditions defined by the specified predicate.
		/// </summary>
		/// <param name="match"></param>
		/// <returns></returns>
		public List<TItem> FindAll(Predicate<TItem> match)
		{
			return ((List<TItem>)this.Items).FindAll(match);
		}

		/// <summary>
		/// Converts the elements in the current List to another type, and returns a list containing the converted elements.
		/// </summary>
		/// <typeparam name="TOutput"></typeparam>
		/// <param name="converter"></param>
		/// <returns></returns>
		public List<TOutput> ConvertAll<TOutput>(Converter<TItem, TOutput> converter)
		{
			return ((List<TItem>)this.Items).ConvertAll<TOutput>(converter);
		}

		/// <summary>
		/// Copies the elements of the System.Collections.Generic.List to a new array.
		/// </summary>
		/// <returns></returns>
		public TItem[] ToArray()
		{
			return ((List<TItem>)this.Items).ToArray();
		}

		/// <summary>
		/// Determines whether every element in the List
		/// matches the conditions defined by the specified predicate.
		/// </summary>
		/// <param name="match"></param>
		/// <returns></returns>
		public bool TrueForAll(Predicate<TItem> match)
		{
			return ((List<TItem>)this.Items).TrueForAll(match);
		}


		#endregion



	}

}
